#--coding: utf-8--

#!/usr/bin/env python2.7
#
#   Copyright (c) 2012-2013, Jude Feng <myfjdthink@163.com>
#   
#

"""
802.1X EAP-MD5 认证请求.
"""

import sys
import socket
from fcntl import ioctl
from select import select
from struct import pack, unpack
from hashlib import md5
from getpass import getpass
from optparse import OptionParser

import logging

pae_group_addr = b"\x01\x80\xc2\x00\x00\x03" #发送广播到目标网卡地址
ETH_P_PAE = 0x888E # Port Access Entity (IEEE 802.1X)

def build_mreq(ifindex):
    
    PACKET_MR_MULTICAST = 0
    return pack('IHH8s', ifindex, PACKET_MR_MULTICAST,
            len(pae_group_addr), pae_group_addr)
def get_ifindex(sock, ifname):
	'''
	选择网卡
	'''
	SIOCGIFINDEX = 0x8933
	ifname, ifindex = unpack('16sI',
            ioctl(sock, SIOCGIFINDEX, pack('16sI', ifname, 0)))
	return ifindex

def get_hwaddr(sock, ifname):
	'''
	获取本机Mac地址
	'''
	SIOCGIFHWADDR = 0x8927
	ifname, sa_family, hwaddr = unpack('16sH6s',
            ioctl(sock, SIOCGIFHWADDR, pack('16sH6s', ifname, 0, '')))
	return hwaddr

def hwaddr_to_str(hwaddr):
	'''
	Mac地址to string
	'''
	return repr(hwaddr)

class MD5Supplicant(object):

    def __init__(self, ifname, user_id=None, user_pw=None, interactive=False):

        self.state = 0

        self.user_id = user_id
        self.user_pw = user_pw

        self.interactive = interactive
	
        self.assoc_hwaddr = None			#认证交换机的网卡地址

        self.ifname = ifname
        self.sock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_PAE))
        self.sock.bind((ifname, 0))
        self.ifindex = get_ifindex(self.sock, self.ifname)
        self.hwaddr = get_hwaddr(self.sock, self.ifname)
	
        SOL_PACKET = 263
        PACKET_ADD_MEMBERSHIP = 1
        self.sock.setsockopt(SOL_PACKET, PACKET_ADD_MEMBERSHIP,
                build_mreq(self.ifindex))
                
    def show_hex(self,hex_oper):
        '''
            打印2进制数据
        '''
        for i in range(0,len(hex_oper)):
            print hex(ord(hex_oper[i])),
        print '\n'

    def get_user_id(self, identity="Identity:"):
        if self.interactive and self.user_id is None:
            self.user_id = input(identity + ' ')
        user = self.user_id + str(b"\x00\x44\x61\x00\x00\xc0\xa5\x17\x0f")
        return user

    def get_user_pw(self, force=False):
        if self.interactive and self.user_pw is None:
            self.user_pw = getpass()
        return self.user_pw

    def make_ether_header(self):
		return pack('>6s6sH', pae_group_addr, self.hwaddr, ETH_P_PAE)

    def make_8021x_header(self, x_type, length=0):
        return pack('>BBH', 1, x_type, length)

    def make_eap_pkt(self, eap_code, eap_id, eap_data):
        return pack('>BBH%ds' % len(eap_data), eap_code, eap_id,
                len(eap_data) + 4, eap_data)

    def send_start(self):
        X_TYPE_START = 1
        pkt = self.make_ether_header()
        pkt += self.make_8021x_header(X_TYPE_START, 0)
        self.sock.send(pkt)
        print '\t勤俭诚信，立己树人'
        print '\t嘉应学院拨号软件For Ubuntu专版，基于Python2.7'
        print '\t============================================='
        logging.info('开始请求登录。')
        self.state = 0

    def handle(self):
	'''
	对交换机发回不同类型到请求作出响应
	'''
        X_TYPE_EAP_PACKET = 0

        EAP_CODE_REQUEST = 1
        EAP_CODE_RESPONSE = 2
        EAP_CODE_SUCCESS = 3
        EAP_CODE_FAILURE = 4

        EAP_TYPE_IDENTITY = 1
        EAP_TYPE_MD5CHALLENGE = 4

        data = self.sock.recv(65535)
        logging.debug('收到数据长度 %d.' % len(data))
        logging.debug("协议类型校验失败")
        ether_dst, ether_src, ether_type = unpack('>6s6sH', data[:14])

        if  ether_type != ETH_P_PAE:
            logging.debug('协议类型不是802.1x: dst=%s type=%s' % (
                 hwaddr_to_str(ether_dst), ether_type))
            return

        if self.assoc_hwaddr == pae_group_addr:
            self.assoc_hwaddr = ether_src
            logging.info('交换机地址： %s.' % hwaddr_to_str(ether_src))
        

        # 802.1X check
        logging.debug("802.1X协议校验")
        a_8021x_ver, a_8021x_type, a_8021x_length = unpack('>BBH', data[14:18])
        if a_8021x_ver != 1 and a_8021x_type != 0:
            logging.debug('802.1X协议校验错误: ver=%d type=%d' % (
                a_8021x_ver, a_8021x_type))
            return

        # EAP length check
        eap_code, eap_id, eap_length = unpack('>BBH', data[18:22])
        if eap_length > len(data) - 18 or eap_length != a_8021x_length:
            logging.debug('EAP长度校验错误: 长度：=%d len(802.1X)=%d' % (
                eap_length, a_8021x_length))
            return
            
	"""
    	数据校验完毕了,那就开始回应了.
	state=0 表示是对登陆请求的回应，发送用户名过去。
	state=1 表示是对密码请求的回应，发送密码过去。
	"""
        if self.state == 0:
            if eap_code == EAP_CODE_REQUEST and eap_length >= 5:
                eap_type = unpack('B', data[22:23])[0]
		
                if eap_type == EAP_TYPE_IDENTITY:
                    logging.debug('交换机请求用户名。')

                    if eap_length > 5:
                        identity = str(unpack('%ds' % (eap_length - 5), data[23:18+eap_length])[0], 'utf-8')
                    else:
                        identity = 'Login:'
		    #发送用户名
                    pkt = self.make_ether_header()
                    user_id = self.get_user_id(identity)
                    eap_pkt = self.make_eap_pkt(EAP_CODE_RESPONSE, eap_id,
                        pack('B%ds' % len(user_id), EAP_TYPE_IDENTITY, user_id))
                    pkt += self.make_8021x_header(X_TYPE_EAP_PACKET, len(eap_pkt))
                    pkt += eap_pkt
                    
                    self.sock.send(pkt)
                    
                    logging.info('用户名已发送.')
                    
                    self.state = 1
            else:
                logging.debug('用户名请求的EAP数据校验错误: code=%d id=%d length=%d type=%d' % (
                    eap_code, eap_id, eap_length, unpack('B', data[22:23])[0]))
        elif self.state == 1:
        
            if eap_code == EAP_CODE_REQUEST and eap_length >= 5:

                eap_type = unpack('B', data[22:23])[0]
 
                if eap_type == EAP_TYPE_MD5CHALLENGE:
                    logging.debug('交换机请求密码')
                    eap_value_size = unpack('B', data[23:24])[0]
		    #嘉应的认证中,MD5-Challenge多了4个字节的Extra data 所以这里要修改一下.
		    if eap_value_size != eap_length - 10:
                        logging.debug('密码请求数据长度错误: size=%d' % eap_value_size)
                        return
                    challenge = data[24:24+eap_value_size]
		    """
		    请求者收到了EAP-Request MD5-Challenge消息后，将消息的Id、自己的密码和消息的Value组合成16进制串，计算MD5值，填入EAP-Response MD5-Challenge，发送给认证者。
		    """		#这里要修改一下	
                    b_eap_id = b"\x02"
                    response = b_eap_id + bytes(self.get_user_pw()) + challenge
                    response = md5(response).digest()
                    #md5 response的拓展部分
                    extra_data = bytes(self.user_id) + b"\x00\x44\x61\x00\x00\xc0\xa8\x17\x0f" #40-59 MD5 extra_data
                    response += extra_data
                    
		    #发送密码
                    pkt = self.make_ether_header()				
                    eap_pkt = self.make_eap_pkt(EAP_CODE_RESPONSE, eap_id,			
                            pack('BB35s', EAP_TYPE_MD5CHALLENGE, 16, response))	
                    pkt += self.make_8021x_header(X_TYPE_EAP_PACKET, len(eap_pkt))
                    pkt += eap_pkt
                    
                    self.sock.send(pkt)
                    

                    logging.info('密码已发送.')
                    
                    self.state = 2
                    return
            elif eap_code == EAP_CODE_FAILURE and eap_length == 4:
                logging.info('用户名不正确.')
                if self.interactive:
                    self.user_id = None
                    self.user_pw = None
                    self.state = 0
                else:
                    raise Exception('Wrong identity, running in non-interactive mode.')
            else:
                logging.debug('请求用户名EAP数据校验错误: code=%d id=%d length=%d type=%d' % (
                    eap_code, eap_id, eap_length, unpack('B', data[22:23])[0]))
                return

        elif self.state == 2:
            if eap_code == EAP_CODE_SUCCESS and eap_length == 4:
                logging.info('认证成功.')
                print '\t============================================='
                self.state = 0
            else:
                logging.info('认证失败!')
                if self.interactive:
                    self.user_pw = None
                    self.state = 0

    def run(self):

        self.send_start()

        while True:
            r, w, x = select([self.sock], [], [self.sock])
            if x:
                raise Exception('socket exception')
            self.handle()

if __name__ == "__main__":

    parser = OptionParser()
    parser.add_option('-i', '--iface', dest='iface', metavar='IFNAME',
            help='network', default='eth0')
    parser.add_option('-u', '--user', dest='user_id', metavar='LOGIN',
            help='username')
    parser.add_option('-p', '--pass', dest='user_pw', metavar='PASSWORD',
            help='password')
    parser.add_option('-e', '--interactive', action='store_true', dest='interactive',
            help='houtai', default=False)
    parser.add_option('-G', '--debug', action='store_true', dest='debug',
            help='debug', default=False)
    parser.add_option('-q', '--quiet', action='store_true', dest='quiet',
            help='quiet', default=False)
    opt, args = parser.parse_args()


    if not opt.interactive:
        for i in (opt.user_id, opt.user_pw):
            if i is None:
                logging.error('用户名和密码必须填。')
                sys.exit(1)

    if opt.quiet:
        loglevel = logging.ERROR
    elif opt.debug:
        loglevel = logging.DEBUG
    else:
        loglevel = logging.INFO

    
	logging.basicConfig(level=loglevel,
		format="%(asctime)s - %(levelname)s - %(funcName)s: %(message)s")

	s = MD5Supplicant(opt.iface, opt.user_id, opt.user_pw, opt.interactive)
	s.run()

